using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.Data.SqlClient;
using Rensoft.Server.IIS;
using System.IO;
using System.Collections.Generic;
using Rensoft.Server.Security;
using System.Security.AccessControl;
using System.Security.Principal;
using Rensoft.Server.DNS;

public class Website
{
    private const VirtualServerAccessFlag accessFlags =
        VirtualServerAccessFlag.AccessRead | VirtualServerAccessFlag.AccessScript;

    private const VirtualServerAuthFlag authFlags =
        VirtualServerAuthFlag.Anonymous;

    private const VirtualServerRedirectFlag redirectFlags =
        VirtualServerRedirectFlag.ExactDestination | VirtualServerRedirectFlag.Permanent;

    private const int mxPriorityIncrease = 10;

    private int websiteId;
    private string websiteGuid;
    private string clientGuid;
    private string title;
    private string[] domainArray;
    private WebsiteType types;
    private DateTime createDate;
    private DateTime updateDate;

    public int WebsiteId
    {
        get { return websiteId; }
        set { websiteId = value; }
    }

    public string WebsiteGuid
    {
        get { return websiteGuid; }
        set { websiteGuid = value; }
    }

    public string ClientGuid
    {
        get { return clientGuid; }
        set { clientGuid = value; }
    }

    public string Title
    {
        get { return title; }
        set { title = value; }
    }

    public string[] DomainArray
    {
        get { return domainArray; }
        set { domainArray = value; }
    }

    public WebsiteType Types
    {
        get { return types; }
        set { types = value; }
    }

    public DateTime CreateDate
    {
        get { return createDate; }
        set { createDate = value; }
    }

    public DateTime UpdateDate
    {
        get { return updateDate; }
        set { updateDate = value; }
    }

    /// <summary>
    /// Parameterless constructor for serialization.
    /// </summary>
    public Website() { }

    protected Website(
        int websiteId,
        string websiteGuid,
        string clientGuid,
        string title,
        string[] domainArray,
        WebsiteType typeFlags,
        DateTime createDate,
        DateTime updateDate)
    {
        this.websiteId = websiteId;
        this.websiteGuid = websiteGuid;
        this.clientGuid = clientGuid;
        this.title = title;
        this.domainArray = domainArray;
        this.types = typeFlags;
        this.createDate = createDate;
        this.updateDate = updateDate;
    }

    internal static Website Create(
        string websiteGuid,
        string clientGuid,
        string title,
        string[] domainArray,
        WebsiteType types)
    {
        int websiteId;

        using (SqlConnection connection = new SqlConnection(Settings.ConnectionStrings.HostingDatabase))
        {
            SqlCommand command = new SqlCommand("WebsiteCreate", connection);
            command.CommandType = CommandType.StoredProcedure;

            command.Parameters.AddWithValue("guid", websiteGuid);
            command.Parameters.AddWithValue("clientGuid", clientGuid);
            command.Parameters.AddWithValue("title", title);
            command.Parameters.AddWithValue("typeFlags", types);

            command.Parameters.Add("websiteId", SqlDbType.Int).Direction = ParameterDirection.Output;

            connection.Open();
            command.ExecuteNonQuery();
            connection.Close();

            websiteId = (int)command.Parameters["websiteId"].Value;
        }

        Website website = new Website(
            websiteId,
            websiteGuid, 
            clientGuid, 
            title,
            domainArray,
            types, 
            DateTime.Now, 
            DateTime.Now);

        if (((types & WebsiteType.Web) != 0) | ((types & WebsiteType.Redirect) != 0))
        {
            website.CreateVirtualServer();
        }

        website.CreateDns();

        return website;
    }

    protected void CreateVirtualServer()
    {
        ClientAccount client = ClientAccount.Fetch(clientGuid);
        WebsiteUser iusrWebsiteUser = client.FetchSingleUser(WebsiteUserType.Anonymous);
        WebsiteUser clientWebsiteUser = client.FetchSingleUser(WebsiteUserType.System);
        VirtualServerManager manager = new VirtualServerManager(Settings.ServerName);

        #region Prepare VS Bindings
        List<VirtualServerBinding> bindingList = new List<VirtualServerBinding>();
        foreach (string domain in domainArray)
        {
            if (Settings.IPAddresses.Length == 1)
            {
                // Use single IP address if only one available - assume port 80 is required.
                bindingList.Add(new VirtualServerBinding(Settings.IPAddresses[0], 80, domain, manager));
            }
            else
            {
                // Otherwise use any available IP (do not create binding for each IP).
                bindingList.Add(new VirtualServerBinding(domain, manager));
            }
        }
        #endregion

        #region Insert Database Records
        using (SqlConnection connection = new SqlConnection(Settings.ConnectionStrings.HostingDatabase))
        {
            SqlCommand command = new SqlCommand("VirtualServerCreate", connection);
            command.CommandType = CommandType.StoredProcedure;

            command.Parameters.AddWithValue("websiteId", websiteId);
            command.Parameters.AddWithValue("accessFlags", accessFlags);
            command.Parameters.AddWithValue("authFlags", authFlags);
            command.Parameters.AddWithValue("redirectFlags", redirectFlags);

            command.Parameters.Add("virtualServerId", SqlDbType.Int).Direction = ParameterDirection.Output;

            connection.Open();

            command.ExecuteNonQuery();

            int virtualServerId = (int)command.Parameters["virtualServerId"].Value;

            SqlCommand bindingsCommand = new SqlCommand("VirtualServerBindingsCreate", connection);
            bindingsCommand.CommandType = CommandType.StoredProcedure;

            bindingsCommand.Parameters.AddWithValue("virtualServerId", virtualServerId);
            bindingsCommand.Parameters.Add("hostname", SqlDbType.NVarChar, 50);
            bindingsCommand.Parameters.Add("ipAddress", SqlDbType.NVarChar, 50);
            bindingsCommand.Parameters.Add("port", SqlDbType.Int);

            bindingsCommand.Parameters.Add(
                "virtualServerBindingId", SqlDbType.Int).Direction = ParameterDirection.Output;

            foreach (VirtualServerBinding binding in bindingList)
            {
                bindingsCommand.Parameters["hostname"].Value = binding.Hostname;
                bindingsCommand.Parameters["port"].Value = binding.Port;

                bindingsCommand.Parameters["ipAddress"].Value = DBNull.Value;
                if (!string.IsNullOrEmpty(binding.IP))
                {
                    bindingsCommand.Parameters["ipAddress"].Value = binding.IP;
                }

                bindingsCommand.ExecuteNonQuery();
            }
            
            connection.Close();
        }
        #endregion

        #region Create Virtual Server
        string description = string.Format(
            Settings.IIS.VirtualServerDescriptionFormat, client.Code, title);

        string directory = client.WebsitesDirectory + Path.DirectorySeparatorChar + title;

        VirtualServer virtualServer = new VirtualServer(
            description,
            directory,
            client.ApplicationPoolName,
            bindingList,
            accessFlags,
            iusrWebsiteUser.ToWindowsUser(),
            VirtualServerPath.Empty);

        manager.Create(virtualServer, true);
        #endregion

        #region Create Directory
        // Allow user modify access to children but not container.
        FileSystemAccessRule modifyRule = new FileSystemAccessRule(
            new SecurityIdentifier(clientWebsiteUser.Sid),
            FileSystemRights.Modify,
            InheritanceFlags.ContainerInherit |
            InheritanceFlags.ObjectInherit,
            PropagationFlags.None,
            AccessControlType.Allow);

        // Allow user to create sub-directories benieth wwwroot.
        FileSystemAccessRule writeRule = new FileSystemAccessRule(
            new SecurityIdentifier(clientWebsiteUser.Sid),
            FileSystemRights.Write, AccessControlType.Allow);

        Directory.CreateDirectory(directory);
        DirectorySecurity security = Directory.GetAccessControl(directory);
        security.AddAccessRule(modifyRule);
        security.AddAccessRule(writeRule);
        Directory.SetAccessControl(directory, security);
        #endregion
    }

    protected void CreateDns()
    {
        #region Prepare DNS Records
        DnsManager manager = new DnsManager(Settings.ServerName);

        DnsZone zone = new DnsZone(
            title,
            ZoneType.PrimaryZone,
            Settings.DNS.PrimaryServer,
            Settings.DNS.ResponsibleParty);

        manager.CreateZone(zone);

        List<DnsRecord> recordList = new List<DnsRecord>();

        // Name server records are always required.
        foreach (string nameServer in Settings.DNS.NameServers)
        {
            recordList.Add(new DnsNsRecord(
                null, nameServer, zone, Settings.DNS.DefaultTTL));
        }

        // Only create website related records for non-email.
        if (((types & WebsiteType.Redirect) != 0) ||
            ((types & WebsiteType.Web) != 0))
        {
            recordList.Add(new DnsCnameRecord("ftp", title, zone, Settings.DNS.DefaultTTL));

            foreach (string domain in domainArray)
            {
                // Reduce full domain name to shorter DNS style record name.
                string recordName = domain.Replace("." + title, String.Empty);

                // Only if name left after replacement, and not the same as the root domain (container).
                if (!String.IsNullOrEmpty(recordName) && (recordName != domain))
                {
                    recordList.Add(new DnsCnameRecord(recordName, title, zone, Settings.DNS.DefaultTTL));
                }
            }

            // May have multiple IP addresses bound in IIS.
            foreach (string ipAddress in Settings.IPAddresses)
            {
                recordList.Add(new DnsARecord(null, ipAddress, zone, Settings.DNS.DefaultTTL));
            }
        }
        
        // If email required, create mail CNAME and MX records.
        if ((types & WebsiteType.Email) != 0)
        {
            DnsCnameRecord mailRecord = new DnsCnameRecord(
                "mail", Settings.DNS.MailHost, zone, Settings.DNS.DefaultTTL);

            DnsCnameRecord webmailRecord = new DnsCnameRecord(
                "webmail", Settings.DNS.WebmailHost, zone, Settings.DNS.DefaultTTL);

            recordList.Add(mailRecord);
            recordList.Add(webmailRecord);

            int priority = mxPriorityIncrease;
            foreach (string mailExchange in Settings.DNS.MailExchanges)
            {
                DnsMxRecord mxRecord = new DnsMxRecord(
                    null, mailExchange, zone, Settings.DNS.DefaultTTL, priority);

                recordList.Add(mxRecord);
                priority += mxPriorityIncrease;
            }
        }
        #endregion

        #region Insert Database Records
        using (SqlConnection connection = new SqlConnection(Settings.ConnectionStrings.HostingDatabase))
        {
            SqlCommand command = new SqlCommand("DnsRecordCreate", connection);
            command.CommandType = CommandType.StoredProcedure;
            command.Parameters.AddWithValue("websiteId", websiteId);
            command.Parameters.Add("typeFlag", SqlDbType.Int);
            command.Parameters.Add("title", SqlDbType.NVarChar, 200);
            command.Parameters.Add("value", SqlDbType.NVarChar, 200);
            command.Parameters.Add("ttl", SqlDbType.Int);
            command.Parameters.Add("priority", SqlDbType.Int);
            command.Parameters.Add("DnsRecordId", SqlDbType.Int).Direction = ParameterDirection.Output;

            connection.Open();

            foreach (DnsRecord record in recordList)
            {
                command.Parameters["typeFlag"].Value = record.DnsType;
                command.Parameters["value"].Value = record.Value;
                command.Parameters["ttl"].Value = record.TTL;

                // Can be NULL where record is at the root of the zone.
                if (!string.IsNullOrEmpty(record.Name))
                {
                    command.Parameters["title"].Value = record.Name;
                }
                else
                {
                    // Database intentionally accepts NULL value.
                    command.Parameters["title"].Value = DBNull.Value;
                }

                // Only MX records require a priority.
                if (record is DnsMxRecord)
                {
                    DnsMxRecord mxRecord = record as DnsMxRecord;
                    command.Parameters["priority"].Value = mxRecord.Priority;
                }
                else
                {
                    // Database intentionally accepts NULL value.
                    command.Parameters["priority"].Value = DBNull.Value;
                }

                command.ExecuteNonQuery();
            }

            connection.Close();
        }
        #endregion

        // Finally, create actual DNS records after the database.
        manager.CreateRecords(recordList);
    }

    internal static List<string> DomainUsage(List<string> domainList)
    {
        List<string> usageList = new List<string>();
        using (SqlConnection connection = new SqlConnection(Settings.ConnectionStrings.HostingDatabase))
        {
            SqlCommand command = new SqlCommand("VirtualServerBindingFetchByHostname", connection);
            command.CommandType = CommandType.StoredProcedure;
            command.Parameters.Add("hostname", SqlDbType.NVarChar, 200);

            connection.Open();
            foreach (string domain in domainList)
            {
                command.Parameters["hostname"].Value = domain;
                SqlDataReader reader = command.ExecuteReader(CommandBehavior.SingleRow);
                if (reader.Read())
                {
                    // Add to usage list if row found.
                    usageList.Add(domain);
                }
                reader.Close();
            }
            connection.Close();
        }
        return usageList;
    }

    internal static Website Fetch(string guid)
    {
        using (SqlConnection connection = new SqlConnection(Settings.ConnectionStrings.HostingDatabase))
        {
            SqlCommand command = new SqlCommand("WebsiteFetchByGuid", connection);
            command.CommandType = CommandType.StoredProcedure;
            command.Parameters.AddWithValue("guid", guid);

            connection.Open();

            SqlDataReader reader = command.ExecuteReader(CommandBehavior.SingleRow);
            if (!reader.Read())
            {
                throw new InvalidOperationException(
                    "Website does not exist where GUID is '" + guid + "'.");
            }

            Website website = new Website(
                (int)reader["WebsiteId"],
                (string)reader["Guid"],
                (string)reader["ClientGuid"],
                (string)reader["Title"],
                new string[] { },
                (WebsiteType)reader["TypeFlags"],
                (DateTime)reader["CreateDate"],
                (DateTime)reader["UpdateDate"]);

            connection.Close();

            return website;
        }
    }

    internal static bool CheckGuidExists(string guid)
    {
        bool guidExists = false;
        using (SqlConnection connection = new SqlConnection(Settings.ConnectionStrings.HostingDatabase))
        {
            SqlCommand command = new SqlCommand("WebsiteFetchByGuid", connection);
            command.CommandType = CommandType.StoredProcedure;
            command.Parameters.AddWithValue("guid", guid);

            connection.Open();

            SqlDataReader reader = command.ExecuteReader(CommandBehavior.SingleRow);
            guidExists = reader.Read();

            connection.Close();
        }
        return guidExists;
    }
}

[Flags]
public enum WebsiteType
{
    Web = 1,
    Email = 2,
    Redirect = 4
}